<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Diff Demo</title>
  <!-- <script src="https://unpkg.com/vue@next"></script> -->
  <style>
    .red {
      color: red
    }

    .green {
      color: green
    }
  </style>
</head>

<body>
  <div id="app"></div>

  <script>
    const isArray = Array.isArray
    const isString = (val) => typeof val === 'string'
    const ShapeFlags = {
      // html 或 svg 标签
      ELEMENT: 1,
      // 函数式组件
      FUNCTIONAL_COMPONENT: 1 << 1,
      // 普通有状态组件
      STATEFUL_COMPONENT: 1 << 2,
      // 子节点是纯文本
      TEXT_CHILDREN: 1 << 3,
      // 子节点是数组
      ARRAY_CHILDREN: 1 << 4,
      // 子节点是 slots
      SLOTS_CHILDREN: 1 << 5,
      // Portal
      PORTAL: 1 << 6,
      // Suspense
      SUSPENSE: 1 << 7,
      // 需要被keepAlive的有状态组件
      COMPONENT_SHOULD_KEEP_ALIVE: 1 << 8,
      // 已经被keepAlive的有状态组件
      COMPONENT_KEPT_ALIVE: 1 << 9,
      // 有状态组件和函数式组件都是“组件”，用 COMPONENT 表示
      // COMPONENT: ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
    }

    ShapeFlags.COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT

    function h(tag, props = null, children = null) {

      let shapeFlag = null
      // 这里为了简化，直接这样判断
      if (typeof tag === 'string') {
        shapeFlag = ShapeFlags.ELEMENT
      } else if (typeof tag === 'object') {
        shapeFlag = ShapeFlags.STATEFUL_COMPONENT
      } else if (typeof tag === 'function') {
        shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
      }

      const vnode = {
        _isVNode: true,
        el: null,
        shapeFlag,
        tag,
        props,
        key: props && props.key !== undefined ? props.key : null,
        children
      }
      normalizeChildren(vnode, vnode.children)
      return vnode
    }

    function normalizeChildren(vnode, children) {
      let type = 0
      if (children == null) {
        children = null
      } else if (Array.isArray(children)) {
        type = ShapeFlags.ARRAY_CHILDREN
      } else if (typeof children === 'object') {
        type = ShapeFlags.SLOTS_CHILDREN
      } else if (typeof children === 'string') {
        children = String(children)
        type = ShapeFlags.TEXT_CHILDREN
      } else if (typeof children === 'number') {
        children = String(children)
        type = ShapeFlags.TEXT_CHILDREN
      }

      // 赋值给 vnode.children 使得修改的 children 能生效，
      // 基本类型修改不会影响原始值
      vnode.children = children
      vnode.shapeFlag |= type
    }

    function mount(vnode, container) {
      if (vnode.tag === null) {
        mountTextElement(vnode, container)
      } else if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
        mountElement(vnode, container)
      } else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
        mountStatefulComponent(vnode, container)
      } else if (vnode.shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) {
        mountFunctionalComponent(vnode, container)
      }
    }

    function mountTextElement(vnode, container) {
      const el = document.createTextNode(vnode.children)
      vnode.el = el
      container.appendChild(el)
    }

    function mountElement(vnode, container) {
      const el = document.createElement(vnode.tag)

      // props
      if (vnode.props) {
        for (const key in vnode.props) {
          const value = vnode.props[key]
          el.setAttribute(key, value)
        }
      }

      // children
      if (vnode.children) {
        if (typeof vnode.children === 'string') {
          el.textContent = vnode.children
        } else {
          vnode.children.forEach(child => {
            mount(child, el)
          })
        }
      }

      vnode.el = el
      container.appendChild(el)
    }

    function mountStatefulComponent(vnode, container) {
      const instance = vnode.tag
      instance.$vnode = instance.render()
      mount(instance.$vnode, container)
      instance.$el = vnode.el = instance.$vnode.el
    }

    function mountFunctionalComponent(vnode, container) {
      const $vnode = vnode.tag()
      mount($vnode, container)
      vnode.el = $vnode.el
    }

    /**
     * @param n1 old VNode
     * @param n2 new VNode
     */
    function patch(n1, n2) {
      if (n1.tag === n2.tag) {
        const el = n2.el = n1.el // (*)
        patchProps(n1, n2, el)
        patchChildren(n1, n2, el)
      } else {
        // replace
      }
    }

    function patchProps(n1, n2, el) {
      // update props
      // `n1` 和 `n2` 都存在有无 `props` 的情况
      const oldProps = n1.props || {}
      const newProps = n2.props || {}
      // add prop or update prop
      for (const key in newProps) {
        const oldVal = oldProps[key]
        const newVal = newProps[key]
        if (newVal !== oldVal) {
          el.setAttribute(key, newVal)
        }
      }

      // remove prop
      for (const key in oldProps) {
        if (!newProps.hasOwnProperty(key)) {
          el.removeAttribute(key)
        }
      }
    }

    function patchChildren(n1, n2, el) {
      // update children
      const oldChildren = n1.children
      const newChildren = n2.children

      if (isString(newChildren)) {
        if (isString(oldChildren)) {
          oldChildren !== newChildren && (el.textContent = newChildren)
        } else {
          el.textContent = newChildren
        }
      } else if (isArray(newChildren)) {
        if (isString(oldChildren)) {
          el.innerHTML = ''
          newChildren.forEach(child => {
            mount(child, el)
          })
        } else if (isArray(oldChildren)) {
          // 我们暂且这样粗暴地判断
          const hasKey = newChildren[0].key !== null && newChildren[0].key !== undefined
          if (!hasKey) {
            patchUnkeyedChildren(oldChildren, newChildren, el)
          } else {
            patchKeyedChildren(oldChildren, newChildren, el)
          }
        }
      }
    }

    /**
 * @param c1 old old children
 * @param c2 new new children
 */
    function patchUnkeyedChildren(c1, c2, container) {
      const commonLen = Math.min(c2.length, c1.length)
      for (let i = 0; i < commonLen; i++) {
        patch(c1[i], c2[i])
      }
      if (c2.length > c1.length) {
        c2.slice(c1.length).forEach(child => {
          mount(child, container)
        })
      } else if (c2.length < c1.length) {
        c1.slice(c2.length).forEach(child => {
          container.removeChild(child.el)
        })
      }
    }

    function patchKeyedChildren(c1, c2, container) {
      let indexArr = []
      let addElements = []
      for (let i = 0; i < c2.length; i++) {
        let find = false
        const newChild = c2[i];
        for (let j = 0; j < c1.length; j++) {
          const oldChild = c1[j];
          if (newChild.key === oldChild.key) {
            patch(oldChild, newChild)
            find = true
            indexArr.push(j)
            break
          }
        }

        if (!find) {
          addElements.push(i)
        }
      }

      // 删除节点
      for (let i = 0; i < c1.length; i++) {
        const exist = c2.find(child => child.key === c1[i].key)
        if (!exist) {
          container.removeChild(c1[i].el)
        }
      }

      sortChildrenElements(c1, c2, container, indexArr)
      insertNewElements(c1, c2, container, addElements)
    }

    // n1.children = [el-a, el-b, el-c] => [el-c, el-b, el-a]
    // indexArr 为新 children 索引列表[2,1,0]
    // 这表示要把旧 children 中索引为1的元素插入到索引为0之前
    function sortChildrenElements(c1, c2, container, indexArr) {
      let index = indexArr.length - 1
      while (index > 0) {
        // 把新children
        const lastChildNode = c1[indexArr[index]].el
        const prevChildNode = c1[indexArr[index - 1]].el
        container.insertBefore(prevChildNode, lastChildNode)
        index--
      }

    }
    function insertNewElements(c1, c2, container, addElements) {
      for (let i = 0; i < addElements.length; i++) {
        const addElementIndex = addElements[i];
        mount(c2[addElementIndex], container)
        if (addElementIndex === 0) {
          container.insertBefore(c2[addElementIndex].el, container.children[0])
        } else {
          container.insertBefore(c2[addElementIndex].el, container.children[addElementIndex - 1].nextSibling)
        }
      }
    }
  </script>
  <script>
    // const vdom1 = h('ul', null, [
    //   h('li', null, 1),
    //   h('li', null, 2),
    //   h('li', null, 3)
    // ])
    // const vdom2 = h('ul', null, [
    //   h('li', null, 3),
    //   h('li', null, 2),
    //   h('li', null, 1),
    //   h('li', null, 4)
    // ])

    const vdom1 = h('ul', null, [
      h('li', { key: 'a', class: 'red' }, 1),
      h('li', { key: 'b' }, 2),
      h('li', { key: 'c' }, 3)
    ])

    const vdom2 = h('ul', null, [
      h('li', { key: 'e' }, 5),
      h('li', { key: 'c' }, 3),
      h('li', { key: 'd' }, 4),
      h('li', { key: 'a', class: 'green' }, 1),
    ])
    mount(vdom1, document.querySelector("#app"))

    setTimeout(() => {
      patch(vdom1, vdom2);
    }, 1000)

  </script>
</body>

</html>